#!/bin/sh

# Installs Sun Java 6, required build tools, Openfire XMPP server, Protocol Buffers
# and Wave Reference Server on Debian/Ubuntu

# Contact: jan.killian at gmail ... bugs/patches/enhancements/requests are welcome

# References:
# http://code.google.com/p/wave-protocol/wiki/Installation
# http://jamespurser.com.au/blog/Wave_Reference_Server_-_A_Startup_Guide
# http://code.google.com/p/wave-protocol/wiki/Certificates
# http://en.wikipedia.org/wiki/X.509
  
# For RedHat (and Debian too) based distros you can use the script by James Purser:
# http://jamespurser.com.au/blog/WRS_Installer_Script

# TODO: 
#
#   * other configuration options, like ports (as needed)
#
#   * install WRS daemon running under dedicated user (when needed)
#
#   * detect if source was actually updated, and rebuild WRS + restart daemon in this case
#     (if unattended/scheduled updates are needed)
#     (does not make much sense until waves become persistent between WRS restarts)
#
#   * read all default settings from an existing installation, not only XMPP secret key
#
#   * pre-configure Openfire (would be only good for fully automatic installations)
#
#   * ejabberd support (feel free to implement it if you need it)




############ functions [1/2] ... used for default settings

dn_get () { ### distinguished_name empty_value [attribute] ... ### \ escaping is not implemented
    [ -z "$3" ] && dn_get "$1" "$2" C ST L O OU CN && return
    local N="$1"; shift
    local E="$1"; shift
    while [ -n "$1" ]; do
        echo "$N" | sed -n "/\b$1\s*=\s*\"[^\"]\+\"/I{s/.*\b$1\s*=\s*\"\([^\"]\+\)\".*/\1/Ip;q200;}" \
            && echo "$N" | sed -n "/\b$1\s*=\s*[^,;+=# \"]\+/I{s/.*\b$1\s*=\s*\([^,;+=# \"]\+\).*/\1/Ip;q200;}" \
            && echo $E
        shift; done
    }
dn_set () { ### [attribute, value] ...
    local O=
    while [ -n "$1" ]; do
        [ -n "$2" ] && { [ -z "$O" ] && O=1 || echo -n ', '; echo -n "$1=$2"; } 
        shift 2
        done 
    }
domain2dn () {
    local C="`echo "$1" | grep -o '\.[a-z]\{2\}$' | cut -c 2-3 | tr a-z A-Z`"
    local O="`echo "$1" | grep -o '\.\w\+\.[a-z]\{2,3\}$' | cut -d. -f2 | titlecase`" 
    local OU="`echo "$1" | grep -o '\.\w\+\.\w\+\.[a-z]\{2,3\}$' | cut -d. -f2 | titlecase`" 
    dn_set C "$C" O "$O" OU "$OU" CN "$1"
    }
dn2subject () { ### \ escaping is not implemented
    echo ", $1" | sed 's/\//\\\//g' \
        | sed 's/\s*[,;+]\s*\(\w\+\)\s*=\s*"\([^"]*\)"/\/\1=\2/g' \
        | sed 's/\s*[,;+]\s*\(\w\+\)\s*=\s*\([^,;+=# "]*\)/\/\1=\2/g'
    }
titlecase () { awk '{printf("%s%s\n",toupper(substr($0,1,1)),substr($0,2))}'; }




############ defaults, usage, option parser

APP="${0##*/}"
KEY=
CER=
DOM="`hostname --fqdn`"
SCN="$DOM"
SCD="`domain2dn "$DOM"`"
XSK= ### will be read from config or generated later if not provided by user
VOF='3.6.4'
VPB='2.1.0'
UPD='true'
INF='true'
LIC='not accepted yet'
WGT='-t1 --connect-timeout=5 --progress=dot:mega'
if [ "$USER" = root ]; then
    SVC=wave
    DST=/opt/wave
    BIN=/usr/bin
    DLD=/var/cache/install
else
    SVC=
    DST=~/.wave
    [ -d ~/bin ] && BIN=~/bin || BIN=
    DLD=~/.install
    fi

USE="Usage: $APP [OPTIONS]

  -C, --certificate FILE    certificate file to use with the wave server;
                            defaults to none

  -K, --key FILE            private key file to use with the certificate;
                            defaults to certificate file with .key extension

  -n, --selfcert NAME       file name for a new self signed certificate
                            to be used when no certificate file is provided;
                            defaults to host fqdn ($SCN)

  -N, --selfcertdn DN       distinguished name for a new self signed certificate
                            'C=CC, ST=State, L=Loc, O=Org, OU=Org unit, CN=host fqdn';
                            defaults to fqdn derivate ($SCD)

  -D, --domain DOMAIN       wave server xmpp domain;
                            defaults to host fqdn ($DOM)

  -X, --xmpp-secret KEY     secret key for wave<->xmpp server communication;
                            defaults to existing key or random string

  -t, --target DIRECTORY    directory for wave server jars and scripts;
                            defaults to ~/.wave (/opt/wave for root)

  -c, --cache DIRECTORY     directory for downloads, repositories and logs;
                            defaults to ~/.install (/var/cache/install for root)

  -b, --bin [DIRECTORY]     directory (in path) for wave client/server run scripts;
                            these scripts are created only if directory is provided;
                            defaults to ~/bin if it exists (/usr/bin for root)

  -s, --service [USER]      username for running wave server as a daemon;
                            the daemon is created only if username is provided;				
                            not implemented yet;				
                            defaults to none (wave for root)

  -d, --download            download only, don't install anything 

  -i, --install             install only, don't check for download updates  

  -I, --no-info             suppress user instructions after installation

  -O, --openxfire VERSION   defaults to '$VOF'

  -P, --protobuf VERSION    defaults to '$VPB' (2.2.0 and trunk need work)

  --accept-sun-java-license pre-accept the license by switch for non-interactive setup 

  -h, --help                print usage and exit
"

OPT="`getopt --options C:K:n:N:D:X:t:c:b::s::diIO:P:h \
--long certificate:,key:,selfcert:,selfcertdn:,domain:,xmpp-secret:,target:,cache:,bin::,service::,download,install,no-info,openfire:,protobuf:,accept-sun-java-license,help \
--name "$APP" -- "$@"`" && eval set -- "$OPT" || exit 2
while true; do
    case "$1" in
        (-C|--certificate)  CER="$2"; [ -z "$KEY" ] && KEY=${CER%.*}.key; shift 2;;
        (-K|--key)          KEY="$2"; shift 2;;
        (-n|--selfcert)     SCN="$2"; shift 2;;
        (-N|--selfcertdn)   SCD="$2"; shift 2;;
        (-D|--domain)       DOM="$2"; shift 2;;
        (-X|--xmpp-secret)  XSK="$2"; shift 2;;
        (-t|--target)       DST="$2"; shift 2;;
        (-n|--cache)        DLD="$2"; shift 2;;
        (-b|--bin)          BIN="$2"; shift 2;; # optional, may be empty, that's ok
        (-s|--service)      SVC="$2"; shift 2;; # optional, may be empty, that's ok
        (-d|--download)     UPD='only'; shift;;
        (-i|--install)      UPD=; shift;;
        (-I|--no-info)      INF=; shift;;
        (-O|--openxfire)    VOF="$2"; shift 2;;
        (-P|--protobuf)     VPB="$2"; shift 2;;
        (-h|--help)         echo "$USE"; exit;;
        (--accept-sun-java-license) LIC='accepted'; shift;;
        (--)  shift; break;;
        (*)   echo "$APP: unknown option: $1" >&2; exit 2;;
        esac
    done

LOG="$DLD"




############ functions [2/2]

log () {
    echo "`date '+[%Y-%m-%d %H:%M:%S]'` $@" >&2
    [ -w "$LOG" ] && echo "`date '+[%Y-%m-%d %H:%M:%S]'` $@" >> "$LOG/$APP.log"
    }
run () { ### [test:] [interactive:|local:|read:] command [options]
    [ "$1" = 'test:' ] && local test=$1 && shift || local test=
    log "... $@"
    if [ "$1" = 'interactive:' -o ! -w "$LOG/$APP.log" ]; then
        [ "$1" = 'interactive:' ] && shift
        "$@" || error $test $? "$1 failed" || return $?;
    elif [ "$1" = 'local:' ]; then
        shift
        "$@" 2>> "$LOG/$APP.log" >&2 || error $test $? "$1 failed: check '$LOG/$APP.log'" || return $?
    elif [ "$1" = 'read:' ]; then
        shift
        "$@" 2>> "$LOG/$APP.log" || error $test $? "$1 failed: check '$LOG/$APP.log'" || return $?
    else
        local O
        O="`"$@" 3>&1 >> "$LOG/$APP.log" 2>&3 3>&-`" \
            && { [ -z "$O" ] || echo "$O" >> "$LOG/$APP.log"; } \
            || error $test $? "${O:-$1 failed}" || return $?
        fi
    }
try () { run test: "$@"; }
output () { run read: "$@"; }  
error () {
    [ "$1" = 'test:' ] && local test=$1 && shift || local test=
    case "$1" in ([0-9]|[1-9][0-9]|[1-2][0-9][0-9]) local exitcode=$1; shift; ;; (*) local exitcode=1; ;; esac
    log "!!! $@ [$exitcode]"
    [ -z "$test" ] && exit $exitcode || return $exitcode
    }


check_directory () { ### [existing:] [writeable:] name dir
    [ "$1" = 'existing:' ] && local E=1 && shift || local E=
    [ "$1" = 'writeable:' ] && local A=w && local N=write && shift || { local A=r; local N=read; }
    [ !  -n "$2" ] && echo "no $1 directory specified" >&2 && return 2
    [ !  -e "$2" ] && { [ -n "$E" ] && echo "$1 directory not found: $2" >&2 && return 3 \
        || mkdir -p "$2" || return $?; }
    [ !  -d "$2" ] && echo "invalid $1 directory: `stat -c '%F %N' "$2"`" >&2 && return 5
    [ ! -$A "$2" ] && echo "$N access denied to $1 directory: `stat -c '%F %N [%A] (%U:%G)' "$2"`" >&2 && return 4 || return 0
    } 
check_file () { ### [existing:] [writeable:] name file
    [ "$1" = 'existing:' ] && local E=1 && shift || local E=
    [ "$1" = 'writeable:' ] && local A=w && local N=write && shift || { local A=r; local N=read; }
    [ !  -n "$2" ] && echo "no $1 file specified" >&2 && return 2
    [ !  -e "$2" ] && { [ -n "$E" ] && echo "$1 file not found: $2" >&2 && return 3 \
        || touch "$2" || return $?; }
    [ !  -f "$2" ] && echo "invalid $1 file: `stat -c '%F %N' "$2"`" >&2 && return 5
    [ ! -$A "$2" ] && echo "$N access denied to $1 file: `stat -c '%F %N [%A] (%U:%G)' "$2"`" >&2 && return 4 || return 0
    } 
check_packages () { ### status, [packages]
    local S=$1; shift
    ! aptitude search `echo "$@" | sed 's/\([^ "]\+\)/^\1$/g'` | grep -v "^$S"
    }
getconf () { ### file, variable, [value] ... if value then compare, else print
    local K V R
    K="`escape "$2"`"
    V="`sed -n "/^\s*$K\s*=/{s/^\s*$K\s*=\s*\(.*\)/\1/p;q200;}" "$1"`"; R=$?
    [ $R = 0 ] && echo "<<< $1: $2 is not set" >&2 && return 201
    [ $R != 200 ] && return $R
    echo "<<< $1: $2 = $V" >&2
    [ -z "$3" ] && echo "$V" || [ "$V" = "$3" ] || return 202
    }
setconf () {
    [ "$1" = 'existing:' ] && local E=1 && shift || local E=
    local K V R O
    K="`escape "$2"`"
    V="`escape "$3"`"
    O="`getconf "$1" "$2" "$3"`"; R=$?
    [ $R = 0 -o $R = 201 -a -n "$E" ] && return $R
    echo ">>> $1: $2 = $3" >&2
    if [ $R = 201 ]; then
        echo "$2=$3" >> "$1" || return $?
    else
        sed -i "s/^\(\s*$K\s*=\s*\).*/\1$V/" "$1" || return $?
        fi
    }
escape () { echo "$1" | sed 's/\([\\./*]\)/\\\1/g'; }


certificate_info () { openssl x509 -text -in "$1"; } 
certificate_test () { ### key, cert, [required subject]
    local K C
    check_file existing: 'private key' "$1" || return $?
    check_file existing: 'certificate' "$2" || return $?
    K="`openssl rsa -in "$1" -modulus -noout`" || return $?
    C="`openssl x509 -modulus -in "$2" -noout`" || return $?
    [ "$K" != "$C" ] && echo "private key and certificate moduli don't match:\nprivate key: $K: $1\ncertificate: $C: $2" >&2 && return 1
    [ -z "$3" ] || openssl x509 -subject -in "$2" -noout | grep "$3" 
    } 


accept_sun_java_license () {
    local S
    S="`mktemp "$DLD/accept_sun_java_license.XXXXXXXXXX" `" || return $?
    chmod +x "$S" || return $?
    echo "Template: shared/accepted-sun-dlj-v1-1\nType: boolean" > "$S.templates" || return $?
    echo "#!/bin/sh -e
        . /usr/share/debconf/confmodule
        db_x_loadtemplatefile \"$S.templates\"
        db_set shared/accepted-sun-dlj-v1-1 true" > "$S" || return $?
    export PATH="$PATH:$DLD"
    sudo "$S"  || return $?
    rm -f "$S" "$S.templates" || return 0  ### no exitcode on script removal error
    } 


III () { ### header [name [version]]
    HDR="$1"
    APP="${2:-${0##*/}}" # provided name or this script basename
    VER="$3"
    echo; log "=== Processing $HDR $VER"
    [ -z "$VER" -o "$VER" = "trunk" ] && PID="${APP}" || PID="${APP}-${VER}"
    SRC="$DLD/${PID}"
    }
II  () { log "--- $@"; }
info_requested () { install_requested && [ -n "$INF" ]; }
update_requested () { [ -n "$UPD" -a "$1" != 'updated' ]; }
install_requested () { [ "$UPD" != 'only' ]; }
rebuild_requested () { [ "$VER" = 'trunk' ] && update_requested; }
has_current_package () { ! update_requested "$1" && [ -f "$DLD/$PKG" ]; }
use_tgz_package () { has_current_package "$1" && run tar -C "$DLD" -zoxf "$DLD/$PKG"; }




############ main

III "installer options"

run check_directory writeable: cache  "$DLD"
run check_directory writeable: target "$DST"
[ -n "$BIN" ] && run check_directory writeable: scripts "$BIN"
[ -n "$CER" ] && run certificate_test "$KEY" "$CER"



III "Sun Java 6 and required build tools"

PKG='ant g++ make autoconf libtool subversion mercurial wget ssh screen openssl'
PKGLIC='sun-java6-jdk sun-java6-fonts'

if install_requested; then

    if try check_packages i $PKGLIC; then
        try check_packages i $PKG || run sudo aptitude install -q -y $PKG
    elif [ "$LIC" = accepted ] && try accept_sun_java_license; then
        run sudo aptitude install -q -y $PKGLIC $PKG
    else
        run interactive: sudo aptitude install -y $PKGLIC $PKG
        fi

    [ -z "$JAVA_HOME" ] && JAVA_HOME="`readlink -e "$(which java))"`" \
        && JAVA_HOME="${JAVA_HOME%%/jre/bin/java}" && JAVA_HOME="${JAVA_HOME%%/bin/java}" \
        && export JAVA_HOME

else
    run sudo aptitude install -q -y -d $PKGLIC $PKG
    fi




III "Openfire XMPP server"   openfire "$VOF"

PKG="${APP}_${VER}_all.deb"
URL="http://www.igniterealtime.org/downloadServlet?filename=$APP/$PKG"

II  "Checking $DLD/$PKG"

[ -e "$DLD/$PKG" -a -z "$UPD" ] || run wget $WGT "$URL" -NP "$DLD"

if install_requested; then

    II  "Installing $DLD/$PKG"

    run sudo dpkg --skip-same-version -i "$DLD/$PKG"

    # run sudo invoke-rc.d $APP restart  ### needed AFTER initial config to allow admin console login

    try sudo grep -i '<setup>true</setup>' /etc/openfire/openfire.xml && COF='configured' || COF=

    fi




III "Protocol Buffers"   protobuf "$VPB"

PKG="$PID.tar.gz"
SPB="$SRC"

if [ "$VER" = trunk ]; then

    URL="http://$APP.googlecode.com/svn/trunk/"

    II  "Checking $SRC"

    if [ -d "$SRC/.svn" ]; then
        update_requested && run svn update --non-interactive "$SRC"
    else
        [ -d "$SRC" ] || use_tgz_package || run svn checkout --non-interactive "$URL" "$SRC"
        fi

else

    URL="http://$APP.googlecode.com/files/$PKG"

    II  "Checking $SRC"

    [ -d "$SRC" ] || use_tgz_package \
        || { run wget $WGT "$URL" -NP "$DLD"; use_tgz_package updated; }

    fi

if install_requested; then

    II  "Building $SRC"

    run local: cd "$SRC"
    rebuild_requested || [ ! -f configure ] && run ./autogen.sh
    rebuild_requested || [ ! -f Makefile ]  && run ./configure --prefix=/usr 
    run make                                                                
    # run make check                                                               

    II  "Installing $SRC"

    run sudo make install

    fi

                                                                  



III "Wave Reference Server"   wave-protocol trunk

PKG="${PID}.tar.gz"
URL="https://$APP.googlecode.com/hg/"
CNF="$SRC/build-proto.properties"


II  "Checking $SRC"

if [ -d "$SRC/.hg" ]; then
    update_requested && run local: cd "$SRC" && run hg update --noninteractive
else
    [ -d "$SRC" ] || use_tgz_package || run hg clone --noninteractive "$URL" "$SRC"
    fi


if install_requested; then

    run local: cd "$SRC"

    if rebuild_requested || ! try getconf "$CNF" protoc_dist "$SPB"; then

        II  "Building $SRC/proto_src"

        run setconf "$CNF" protoc_dist "$SPB"
	run find ./proto_src/ -name '*.java' -type f -print -delete 
        run ant -f build-proto.xml
	run ant clean

	fi
 

    II  "Building $SRC"

    run ant


    II  "Checking previous settings"

    CNF="$DST/run-config.sh"

    if [ -z "$XSK" ]; then 
        [ -f "$CNF" ] \
             && XSK="`output getconf "$CNF" XMPP_SERVER_SECRET`" \
             && [ -n "$XSK" ] || XSK="`< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c12`"
        fi


    II  "Installing $SRC to $DST"

    run cp -vupft "$DST" "$SRC"/dist/* "$SRC"/run-*.sh
    run cp -vupfT "$SRC/run-config.sh.example" "$CNF" 


    II  "Checking certificates"

    if [ -z "$CER" ]; then

        CER="$DST/$SCN.cert"
        KEY="$DST/$SCN.key"
        SUB="`dn2subject "$SCD"`"

        if [ ! -e "$CER" -a ! -e "$KEY" ] \
            || ! try certificate_test "$KEY" "$CER" "$SUB" ;then
            ### -a above is intentional for logging if only one exists
            output openssl genrsa 1024 | run openssl pkcs8 -topk8 -nocrypt -out "$KEY" || exit $?
            run openssl req -new -x509 -nodes -sha1 -days 365 -subj "$SUB" -key "$KEY" -out "$CER"
            run certificate_info "$CER"
            fi
    else
        run certificate_info "$CER"
        fi


    II  "Configuring $DST"

    _="`escape "$DST/"`"
    run sed -i "s/dist\//$_/g;s/^\(.*[^/]\)\?\(run-config\.sh\)/\1$_\2/g" "$DST"/run-*.sh

    VWS="`output getconf "$SRC/build.properties" 'fedone.version'`" || exit $?

    run setconf "$CNF" WAVE_SERVER_DOMAIN_NAME     "$DOM"
    run setconf "$CNF" WAVE_SERVER_HOSTNAME        'localhost'
    run setconf "$CNF" WAVE_SERVER_PORT            '9876'
    run setconf "$CNF" FEDONE_VERSION              "$VWS"

    run setconf "$CNF" XMPP_SERVER_SECRET          "$XSK"
    run setconf "$CNF" PRIVATE_KEY_FILENAME        "$KEY"
    run setconf "$CNF" CERTIFICATE_FILENAME_LIST   "$CER"

  # run setconf "$CNF" CERTIFICATE_DOMAIN_NAME     '$WAVE_SERVER_DOMAIN_NAME'
  # run setconf "$CNF" XMPP_SERVER_HOSTNAME        '$WAVE_SERVER_DOMAIN_NAME'
  # run setconf "$CNF" XMPP_SERVER_PORT            '5275'

    run setconf "$CNF" XMPP_SERVER_IP              'localhost'  ### '$XMPP_SERVER_HOSTNAME'

  # run setconf "$CNF" WAVESERVER_DISABLE_VERIFICATION        'false'
    ### set true to disable the verification of signed deltas
  # run setconf "$CNF" WAVESERVER_DISABLE_SIGNER_VERIFICATION 'true'
    ### set true to disable the verification of signers (certificates)

    run sed -i "/^\(\s*[^#].*\)\?exit\s\+[1-9]/s/\(.*\)/# Configured on `date '+%Y-%m-%d %H:%M:%S'` ### \1/" "$CNF"


    if [ -n "$BIN" ]; then

        II  "Symlinking scripts to $BIN"

        for _ in "$DST"/run-*.sh; do
            [ "$_" = "$CNF" ] && continue
            _2="$BIN/wave-${_##*/run-}"
            _2="${_2%.sh}"
            run ln -svnf "$_" "$_2"
            done
        fi
    fi


II  "Done."

if info_requested; then

    echo "\nFurther steps:\n"

    if [ -z "$COF" ]; then

        echo "  * Configure the Openfire XMPP server:"
        echo
        echo "     1. Navigate your browser to https://`hostname --fqdn`:9091/" 
        echo "     2. Follow the initial configuration wizard" 
        echo 
        echo "      + Use the full domain name for server name:"
        echo "        '$DOM'"
        echo 
        echo "     3. Restart the server from console: 'sudo invoke-rc.d openfire restart'" 
        echo "     4. Login to the web console and configure the server" 
        echo 
        echo "      + Use the following shared secret key for the wave external component:"
        echo "        '$XSK'"
        echo
        echo "      + Configuration guides:" 
        echo "        http://jamespurser.com.au/blog/Wave_Reference_Server_-_A_Startup_Guide"
        echo "        http://code.google.com/p/wave-protocol/wiki/Installation"
        echo
        echo "     5. Restart the server from console: 'sudo invoke-rc.d openfire restart'" 
                
    else
        echo "  * You can change the Openfire XMPP server settings at"
        echo "    https://`hostname --fqdn`:9091/" 
        fi

    echo
    echo "  * You can start the wave server using $DST/run-server.sh"
    [ -n "$BIN" ] && echo "    or $BIN/wave-server"

    echo
    echo "  * You can start the console client using $DST/run-client-console.sh"
    [ -n "$BIN" ] && echo "    or $BIN/wave-client-console"

    echo

    fi


